﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Application
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Globalization;
    using System.Linq;
    using System.Windows.Forms;

    using Mews.Application.Controls;
    using Mews.Application.Properties;
    using Mews.Mime;
    using Mews.Net.Nntp.Cache;

    [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1601:PartialElementsMustBeDocumented", Justification = "Another part already documents the class.")]
    internal partial class MainForm
    {
        /// <summary>Implements the controller for groups.</summary>
        private sealed class GroupController : Controller, IController
        {
            private static readonly Color Normal = SystemColors.WindowText;
            private static readonly Color Watched = Color.Red;

            private readonly QueryCache queryCache;
            private readonly string tempDirectory;
            private readonly MenuItem contextMarkAllAsReadMenuItem;
            private readonly MenuItem contextWatchAllMenuItem;
            private ShowArticleForm articleForm;

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            /// <summary>See <see cref="IController.Document"/>.</summary>
            public object Document
            {
                get { return this.queryCache.Group; }
            }

            /// <summary>See <see cref="IController.UpdateCount"/>.</summary>
            public void UpdateCount()
            {
                this.MainForm.groupThreadCounts[this.queryCache.Group] = this.queryCache.GetThreadCount();
            }

            /// <summary>See <see cref="IController.Update"/>.</summary>
            public void Update()
            {
                this.MainForm.treeView.AfterCollapse -= this.OnTreeView_AfterCollapse;
                this.MainForm.treeView.AfterExpand -= this.OnTreeView_AfterExpand;

                try
                {
                    this.ClearTreeViewNodes();
                    bool isNullFilter = this.queryCache.Group.InboundArticleFilter == InboundArticleFilter.NullFilter;
                    this.MainForm.statusTextBox.Text = string.Format(
                        CultureInfo.CurrentCulture,
                        isNullFilter ? Resources.GroupStatus : Resources.GroupStatusFiltered,
                        this.queryCache.Group.DisplayName,
                        AddArticles(this.queryCache.GetOriginalInboundArticles(), this.MainForm.treeView.Nodes));
                }
                finally
                {
                    this.MainForm.treeView.AfterExpand += this.OnTreeView_AfterExpand;
                    this.MainForm.treeView.AfterCollapse += this.OnTreeView_AfterCollapse;
                }

                UpdateWatchAllMenuItem(
                    this.MainForm.quickMenuItem, GetArticle(this.MainForm.treeView.SelectedNode));
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            internal GroupController(MainForm mainForm, IGroup group, string tempDirectory) : base(mainForm)
            {
                this.queryCache = group.CreateQueryCache();
                this.tempDirectory = tempDirectory;

                this.MainForm.treeView.ShowLines = true;

                this.MainForm.quickMenuItem.Text = Resources.WatchAll;

                this.MainForm.sendReceiveMinimalGroupMenuItem.Enabled = true;
                this.MainForm.sendReceiveMinimalHostMenuItem.Enabled = true;
                this.MainForm.sendReceiveFullGroupMenuItem.Enabled = true;
                this.MainForm.sendReceiveFullHostMenuItem.Enabled = true;
                this.MainForm.filterMenuItem.Enabled = true;
                this.MainForm.markGroupAsReadMenuItem.Enabled = true;
                this.MainForm.newMenuItem.Enabled = true;

                this.MainForm.sendReceiveMinimalGroupMenuItem.Click += this.OnSendReceiveMinimalGroupMenuItem_Click;
                this.MainForm.sendReceiveMinimalHostMenuItem.Click += this.OnSendReceiveMinimalHostMenuItem_Click;
                this.MainForm.sendReceiveFullGroupMenuItem.Click += this.OnSendReceiveFullGroupMenuItem_Click;
                this.MainForm.sendReceiveFullHostMenuItem.Click += this.OnSendReceiveFullHostMenuItem_Click;

                this.MainForm.filterMenuItem.Popup += this.OnFilterMenuItem_Popup;
                this.MainForm.allThreadsMenuItem.Click += this.OnFilterChoiceMenuItem_Click;
                this.MainForm.watchedMenuItem.Click += this.OnFilterChoiceMenuItem_Click;
                this.MainForm.unreadMenuItem.Click += this.OnFilterChoiceMenuItem_Click;
                this.MainForm.watchedAndUnreadMenuItem.Click += this.OnFilterChoiceMenuItem_Click;
                this.MainForm.interestingMenuItem.Click += this.OnFilterChoiceMenuItem_Click;

                this.MainForm.markGroupAsReadMenuItem.Click += this.OnMarkGroupAsReadMenuItem_Click;
                this.MainForm.newMenuItem.Click += this.OnNewMenuItem_Click;

                this.MainForm.treeView.AfterExpand += this.OnTreeView_AfterExpand;
                this.MainForm.treeView.AfterCollapse += this.OnTreeView_AfterCollapse;

                this.contextMarkAllAsReadMenuItem = new MenuItem();
                this.contextWatchAllMenuItem = new MenuItem();
                this.MainForm.contextMenu.MenuItems.Add(this.contextMarkAllAsReadMenuItem);
                this.MainForm.contextMenu.MenuItems.Add(this.contextWatchAllMenuItem);
                this.contextMarkAllAsReadMenuItem.Click += this.OnMarkAllAsReadMenuItem_Click;
                this.contextWatchAllMenuItem.Click += this.OnQuickMenuItem_Click;
            }

            internal sealed override void OnQuickMenuItem_Click(object sender, EventArgs e)
            {
                ToggleIsBeingWatched(
                    this.MainForm.treeView.SelectedNode,
                    GetArticle(this.MainForm.treeView.SelectedNode),
                    this.MainForm.quickMenuItem);
            }

            internal sealed override void OnTreeView_AfterSelect(object sender, TreeViewEventArgs e)
            {
                var article = GetArticle(e.Node);
                article.IsSelected = true;
                UpdateWatchAllMenuItem(this.MainForm.quickMenuItem, article);
            }

            internal sealed override void OnTreeView_KeyDown(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.Enter)
                {
                    e.Handled = true;
                    this.ShowArticleForm();
                }
            }

            internal sealed override void OnTreeView_NodeTap(object sender, EventArgs e)
            {
                this.ShowArticleForm();
            }

            internal sealed override void OnContextMenu_Popup(object sender, EventArgs e)
            {
                var selectedArticle = GetArticle(this.MainForm.treeView.SelectedNode);

                UpdateMarkAllAsReadMenuItem(this.contextMarkAllAsReadMenuItem, selectedArticle);
                UpdateWatchAllMenuItem(this.contextWatchAllMenuItem, selectedArticle);
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            /// <summary>See <see cref="Controller.Dispose(bool)"/>.</summary>
            protected sealed override void Dispose(bool disposing)
            {
                try
                {
                    this.contextWatchAllMenuItem.Click -= this.OnQuickMenuItem_Click;
                    this.contextMarkAllAsReadMenuItem.Click -= this.OnMarkAllAsReadMenuItem_Click;
                    this.contextWatchAllMenuItem.Dispose();
                    this.contextMarkAllAsReadMenuItem.Dispose();

                    this.MainForm.treeView.AfterCollapse -= this.OnTreeView_AfterCollapse;
                    this.MainForm.treeView.AfterExpand -= this.OnTreeView_AfterExpand;

                    this.MainForm.newMenuItem.Click -= this.OnNewMenuItem_Click;
                    this.MainForm.markGroupAsReadMenuItem.Click -= this.OnMarkGroupAsReadMenuItem_Click;

                    this.MainForm.interestingMenuItem.Click -= this.OnFilterChoiceMenuItem_Click;
                    this.MainForm.watchedAndUnreadMenuItem.Click -= this.OnFilterChoiceMenuItem_Click;
                    this.MainForm.unreadMenuItem.Click -= this.OnFilterChoiceMenuItem_Click;
                    this.MainForm.watchedMenuItem.Click -= this.OnFilterChoiceMenuItem_Click;
                    this.MainForm.allThreadsMenuItem.Click -= this.OnFilterChoiceMenuItem_Click;
                    this.MainForm.filterMenuItem.Popup -= this.OnFilterMenuItem_Popup;

                    this.MainForm.sendReceiveFullHostMenuItem.Click -= this.OnSendReceiveFullHostMenuItem_Click;
                    this.MainForm.sendReceiveFullGroupMenuItem.Click -= this.OnSendReceiveFullGroupMenuItem_Click;
                    this.MainForm.sendReceiveMinimalHostMenuItem.Click -= this.OnSendReceiveMinimalHostMenuItem_Click;
                    this.MainForm.sendReceiveMinimalGroupMenuItem.Click -= this.OnSendReceiveMinimalGroupMenuItem_Click;

                    if (this.articleForm != null)
                    {
                        this.articleForm.Dispose();
                        this.articleForm = null;
                    }

                    this.queryCache.Dispose();
                }
                finally
                {
                    base.Dispose(disposing);
                }
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            ////////// MainMenu handlers //////////

            private void OnSendReceiveMinimalGroupMenuItem_Click(object sender, EventArgs e)
            {
                this.StartGroupSendReceive(false);
            }

            private void OnSendReceiveMinimalHostMenuItem_Click(object sender, EventArgs e)
            {
                this.StartHostSendReceive(false);
            }

            private void OnSendReceiveFullGroupMenuItem_Click(object sender, EventArgs e)
            {
                this.StartGroupSendReceive(true);
            }

            private void OnSendReceiveFullHostMenuItem_Click(object sender, EventArgs e)
            {
                this.StartHostSendReceive(true);
            }

            private void OnFilterMenuItem_Popup(object sender, EventArgs e)
            {
                this.SetChecked(this.MainForm.allThreadsMenuItem);
                this.SetChecked(this.MainForm.watchedMenuItem);
                this.SetChecked(this.MainForm.unreadMenuItem);
                this.SetChecked(this.MainForm.watchedAndUnreadMenuItem);
                this.SetChecked(this.MainForm.interestingMenuItem);
            }

            private void OnFilterChoiceMenuItem_Click(object sender, EventArgs e)
            {
                this.queryCache.Group.InboundArticleFilter = this.GetFilter((MenuItem)sender);
                this.MainForm.update(false);
            }

            private void OnMarkGroupAsReadMenuItem_Click(object sender, EventArgs e)
            {
                this.MainForm.ExecuteAndUpdate(this.queryCache.Group.MarkAllInboundArticlesAsRead, false);
            }

            private void OnNewMenuItem_Click(object sender, EventArgs e)
            {
                this.CreateArticle(this.MainForm, null, null, null);
            }

            ////////// this.treeView handlers //////////

            private void OnTreeView_AfterExpand(object sender, TreeViewEventArgs e)
            {
                // The previous version of this method relied on AfterExpand events recursively being fired for all
                // nodes. For deeply nested threads this reproducibly crashed the application without any sort of
                // diagnostic, which I've so far only seen when a stack overflow occurs.
                // This new version therefore avoids the event firing by removing the handler first and then adding it
                // back later.
                this.MainForm.treeView.AfterExpand -= this.OnTreeView_AfterExpand;

                try
                {
                    Expand(e.Node);
                }
                finally
                {
                    this.MainForm.treeView.AfterExpand += this.OnTreeView_AfterExpand;
                }
            }

            private void OnTreeView_AfterCollapse(object sender, TreeViewEventArgs e)
            {
                GetArticle(e.Node).IsExpanded = false;
            }

            ////////// this.ContextMenu handlers //////////

            private void OnMarkAllAsReadMenuItem_Click(object sender, EventArgs e)
            {
                var selectedNode = this.MainForm.treeView.SelectedNode;
                var selectedArticle = GetArticle(selectedNode);
                var newValue = !selectedArticle.HasBeenRead;
                this.ModifyThread(
                    this.MainForm.treeView.SelectedNode, article => article.HasBeenRead = newValue);
            }

            ////////// Helper methods //////////

            private void StartGroupSendReceive(bool isFullReceive)
            {
                this.MainForm.cache.StartSendReceive(
                    new IGroup[] { this.queryCache.Group }, isFullReceive, this.MainForm);
            }

            private void StartHostSendReceive(bool isFullReceive)
            {
                this.MainForm.cache.StartSendReceive(
                    this.queryCache.Group.Host.GetGroups().ToArray(), isFullReceive, this.MainForm);
            }

            private void SetChecked(MenuItem menuItem)
            {
                menuItem.Checked = this.queryCache.Group.InboundArticleFilter == this.GetFilter(menuItem);
            }

            private InboundArticleFilter GetFilter(MenuItem filter)
            {
                if (filter == this.MainForm.watchedMenuItem)
                {
                    return new InboundArticleFilter(true, null, true, null);
                }
                else if (filter == this.MainForm.unreadMenuItem)
                {
                    return new InboundArticleFilter(null, false, null, false);
                }
                else if (filter == this.MainForm.watchedAndUnreadMenuItem)
                {
                    return new InboundArticleFilter(true, false, true, false);
                }
                else if (filter == this.MainForm.interestingMenuItem)
                {
                    return new InboundArticleFilter(null, false, true, false);
                }
                else
                {
                    return InboundArticleFilter.NullFilter;
                }
            }

            private void ShowArticleForm()
            {
                var node = this.MainForm.treeView.SelectedNode;

                if (node != null)
                {
                    if (this.articleForm == null)
                    {
                        this.articleForm = new ShowArticleForm(this.MainForm, this.tempDirectory);
                    }

                    var article = GetArticle(node);
                    this.articleForm.ShowDialog(
                        () => ToggleIsBeingWatched(node, article, this.MainForm.quickMenuItem),
                        newValue => SetHasBeenRead(node, article, newValue),
                        (owner, subject, body, parentMessage) =>
                            this.CreateArticle(owner, subject, body, parentMessage),
                        newValue => SetHasAttachments(node, article, newValue),
                        article.GetContent(),
                        article.IsBeingWatched);
                }
            }

            private void CreateArticle(Form owner, string subject, string body, Message parentMessage)
            {
                var group = this.queryCache.Group;

                if (string.IsNullOrEmpty(group.Host.EmailAddress))
                {
                    var text = string.Format(CultureInfo.InvariantCulture, Resources.EmailRequired, group.Host.Name);
                    MessageBoxHelper.ShowError(text, Resources.EmailRequiredCaption);
                    return;
                }

                using (var form = new EditArticleForm(
                    owner, false, group.Name, subject, this.MainForm.cache.ArticleSubjectMaxLength, body))
                {
                    var dialogResult = form.ShowDialog();

                    if (dialogResult != DialogResult.Cancel)
                    {
                        var article = group.Host.AddOutboundArticle();
                        article.SaveMessage(
                            new Message()
                            {
                                Fields = { new UnstructuredHeaderField("Newsgroups") { Body = group.Name } },
                                From = new List<Address>() { new Address(group.Host.UserName, group.Host.EmailAddress) },
                                Subject = form.Subject,
                                References = GetChildReferences(parentMessage),
                                Body = new TextBody(TextSubtypeNames.Plain) { Content = form.Body }
                            });
                        article.SendStatus = dialogResult == DialogResult.Yes ? SendStatus.Outbox : SendStatus.Draft;
                        this.MainForm.UpdateOutboundArticleCounts();
                    }
                }
            }

            private void ModifyThread(TreeNode node, Action<IInboundArticle> action)
            {
                this.MainForm.treeView.BeginUpdate();

                try
                {
                    ModifyThreadCore(node, action);
                }
                finally
                {
                    this.MainForm.treeView.EndUpdate();
                }
            }

            private static void Expand(TreeNode node)
            {
                node.Expand();
                GetArticle(node).IsExpanded = true;

                foreach (TreeNode childNode in node.Nodes)
                {
                    Expand(childNode);
                }
            }

            private static IInboundArticle GetArticle(TreeNode node)
            {
                return (IInboundArticle)(node == null ? null : node.Tag);
            }

            private static bool ToggleIsBeingWatched(TreeNode node, IInboundArticle article, MenuItem watchAllMenuItem)
            {
                var newValue = !article.IsBeingWatched;
                ModifyThreadCore(node, article2 => article2.IsBeingWatched = newValue);
                UpdateWatchAllMenuItem(watchAllMenuItem, article);
                return newValue;
            }

            private static void ModifyThreadCore(TreeNode node, Action<IInboundArticle> action)
            {
                var article = (IInboundArticle)node.Tag;
                action(article);
                SetNodeAppearance(node, article);

                foreach (TreeNode childNode in node.Nodes)
                {
                    ModifyThreadCore(childNode, action);
                }
            }

            private static void UpdateWatchAllMenuItem(MenuItem watchAllMenuItem, IInboundArticle selectedArticle)
            {
                bool isArticleSelected = selectedArticle != null;
                watchAllMenuItem.Enabled = isArticleSelected;
                watchAllMenuItem.Text =
                    isArticleSelected && selectedArticle.IsBeingWatched ? Resources.WatchNone : Resources.WatchAll;
            }

            private static void UpdateMarkAllAsReadMenuItem(
                MenuItem markAllAsReadMenuItem, IInboundArticle selectedArticle)
            {
                bool isArticleSelected = selectedArticle != null;
                markAllAsReadMenuItem.Enabled = isArticleSelected;
                markAllAsReadMenuItem.Text = isArticleSelected && selectedArticle.HasBeenRead ?
                    Resources.MarkAllAsUnread : Resources.MarkAllAsRead;
            }

            private static int AddArticles(IEnumerable<IInboundArticle> articles, TreeNodeCollection treeNodes)
            {
                int articleCount = 0;

                foreach (var article in articles)
                {
                    TreeNode node = new TreeNode(article.Subject);
                    node.Tag = article;
                    treeNodes.Add(node);
                    ++articleCount;

                    AddArticles(article.GetFollowupInboundArticles(), node.Nodes);
                    SetNodeAppearance(node, article);

                    if (article.IsExpanded)
                    {
                        node.Expand();
                    }

                    if (article.IsSelected)
                    {
                        node.TreeView.SelectedNode = node;
                    }
                }

                return articleCount;
            }

            private static void SetHasBeenRead(TreeNode node, IInboundArticle article, bool newValue)
            {
                article.HasBeenRead = newValue;
                SetNodeAppearance(node, article);
            }

            private static void SetHasAttachments(TreeNode node, IInboundArticle article, bool newValue)
            {
                if (article.HasAttachments == null)
                {
                    article.HasAttachments = newValue;
                    SetNodeAppearance(node, article);
                }
            }

            private static IList<string> GetChildReferences(Message parentMessage)
            {
                if (parentMessage != null)
                {
                    // See http://www.ietf.org/rfc/rfc5322.txt, 3.6.4.
                    var childReferences = new List<string>();

                    if (parentMessage.References != null)
                    {
                        foreach (var reference in parentMessage.References)
                        {
                            childReferences.Add(reference);
                        }
                    }
                    else
                    {
                        if ((parentMessage.InReplyTo != null) && (parentMessage.InReplyTo.Count == 1))
                        {
                            childReferences.Add(parentMessage.InReplyTo[0]);
                        }
                    }

                    if (parentMessage.MessageId != null)
                    {
                        childReferences.Add(parentMessage.MessageId);
                    }

                    return childReferences;
                }

                return null;
            }

            private static void SetNodeAppearance(TreeNode node, IInboundArticle article)
            {
                // The full article must be available to reliably determine whether it has attachments. HasAttachments
                // can therefore only be true when ReceiveStatus == ReceiveStatus.FullArticle, which is why assigning it
                // 4 is enough.
                int imageIndex = (article.HasBeenRead ? 1 : 0) + (article.IsBeingWatched ? 2 : 0) +
                    (article.ReceiveStatus == ReceiveStatus.FullArticle ? 4 : 0) +
                    (article.HasAttachments == true ? 4 : 0);
                node.SetAppearance(!article.HasBeenRead, imageIndex, imageIndex);
                node.ForeColor = article.IsBeingWatched ? Watched : Normal;
            }
        }
    }
}
